package org.barad.architecture.analyzer.archpad.model;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.log4j.Logger;
import org.barad.architecture.analyzer.archpad.component.ArchPadComponent;
import org.barad.architecture.analyzer.archpad.component.annotations.ModelProperty;

/**
 * This class is a manager for component property descriptors.
 * It maintains a cache that maps a component class to a set of
 * model properties represented via {@link ModelPropertyDescriptor}s.
 * 
 * @author svetoslavganov@gmial.com (Svetoslav R. Ganov)
 *
 */
public class ModelPropertyDescriptorManager {
	private static ModelPropertyDescriptorManager manager;
	private static Logger log = Logger.getLogger(ModelPropertyDescriptorManager.class); 
	private Map<Class<?>, Set<ModelPropertyDescriptor>> propertyDescriptorCache = 
			new HashMap<Class<?>, Set<ModelPropertyDescriptor>>();
	
	public static ModelPropertyDescriptorManager getInstance() {
		if (manager == null) {
			manager = new ModelPropertyDescriptorManager();
		}
		return manager;	
	}
	
	/**
	 * Gets the {@link ModelPropertyDescriptor}s of a class via reflection.
	 * 
	 * @param clazz The class.
	 * @return The {@link ModelPropertyDescriptor}s.
	 */
	public Set<ModelPropertyDescriptor> getModelPropertyDescriptors(Class<?> clazz) {
		Set<ModelPropertyDescriptor> descriptors = propertyDescriptorCache.get(clazz);
		if (descriptors == null) {
			descriptors = collectModelPropertyDescriptors(clazz);
			propertyDescriptorCache.put(clazz, descriptors);
		}
		return propertyDescriptorCache.get(clazz);
	}
	
	/**
	 * Collects the {@link ModelPropertyDescriptor}s of a class via reflection.
	 * 
	 * @param clazz The class.
	 * @return The {@link ModelPropertyDescriptor}s.
	 */
	private Set<ModelPropertyDescriptor> collectModelPropertyDescriptors(Class<?> clazz) {
		Class<?> currentClass = clazz;
		Set<ModelPropertyDescriptor> descriptors = 
			new LinkedHashSet<ModelPropertyDescriptor>();
		while (currentClass != null && !currentClass.equals(ArchPadComponent.class)) {
			Field[] declaredFields = currentClass.getDeclaredFields();
			for (Field field: declaredFields) {
				if (field.isAnnotationPresent(ModelProperty.class)) {
					ModelPropertyDescriptor descriptor = new ModelPropertyDescriptor();
					descriptor.setField(field);
					addGetterAndSetter(descriptor, currentClass);
					descriptors.add(descriptor);
				}
			}
			currentClass = currentClass.getSuperclass();
		}
		return descriptors;
	}
	
	/**
	 * Adds the getter and setter of a model property to its
	 * {@link ModelPropertyDescriptor}.
	 * 
	 * @param descriptor The {@link ModelPropertyDescriptor}s.
	 * @param clazz The class owner of the property.
	 */
	private void addGetterAndSetter(ModelPropertyDescriptor descriptor, 
			Class<?> clazz) {
		Field field = descriptor.getField();
		StringBuilder baseName = new StringBuilder(field.getName());
		baseName.replace(0, 1, baseName.substring(0, 1).toUpperCase());
		try {
			Method getter = clazz.getDeclaredMethod("get" + baseName.toString(), 
					new Class<?>[]{});
			descriptor.setGetter(getter);
			Class<?> returnType = getter.getReturnType();
			Method setter = clazz.getDeclaredMethod("set" + baseName.toString(), 
					new Class<?>[]{returnType});
			descriptor.setSetter(setter);
		} catch (NoSuchMethodException nsme) {
			log.error("Component class " + clazz.getName() + 
					" does not follow Java Beans naming convention");
		}
	}
	
	/**
	 * This class describes a model property.
	 * 
	 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
	 *
	 */
	public static class ModelPropertyDescriptor {
		private Field field;
		private Method getter;
		private Method setter;
		
		/**
		 * Gets the {@link Field}.
		 * 
		 * @return The {@link Field}.
		 */
		public Field getField() {
			return field;
		}
		
		/**
		 * Sets the {@link Field}.
		 * 
		 * @param field The {@link Field}.
		 */
		public void setField(Field field) {
			this.field = field;
		}
		
		/**
		 * Gets the getter {@link Method}.
		 * 
		 * @return The getter.
		 */
		public Method getGetter() {
			return getter;
		}
		
		/**
		 * Sets the getter {@link Method}.
		 * 
		 * @param getter The getter.
		 */
		public void setGetter(Method getter) {
			this.getter = getter;
		}
		
		/**
		 * Sets the setter method.
		 * 
		 * @return The setter.
		 */
		public Method getSetter() {
			return setter;
		}
		
		/**
		 * Sets the setter {@link Method}.
		 * 
		 * @param setter The setter.
		 */
		public void setSetter(Method setter) {
			this.setter = setter;
		}
	}
}
